import BoundingRectangle from '../Core/BoundingRectangle.js';
import Color from '../Core/Color.js';
import defined from '../Core/defined.js';
import destroyObject from '../Core/destroyObject.js';
import PixelFormat from '../Core/PixelFormat.js';
import WebGLConstants from '../Core/WebGLConstants.js';
import ClearCommand from '../Renderer/ClearCommand.js';
import DrawCommand from '../Renderer/DrawCommand.js';
import Framebuffer from '../Renderer/Framebuffer.js';
import PixelDatatype from '../Renderer/PixelDatatype.js';
import RenderState from '../Renderer/RenderState.js';
import ShaderSource from '../Renderer/ShaderSource.js';
import Texture from '../Renderer/Texture.js';
import AdjustTranslucentFS from '../Shaders/AdjustTranslucentFS.js';
import CompositeOITFS from '../Shaders/CompositeOITFS.js';
import BlendEquation from './BlendEquation.js';
import BlendFunction from './BlendFunction.js';

    /**
     * @private
     */
    function OIT(context) {
        // We support multipass for the Chrome D3D9 backend and ES 2.0 on mobile.
        this._translucentMultipassSupport = false;
        this._translucentMRTSupport = false;

        var extensionsSupported = context.colorBufferFloat && context.depthTexture;
        this._translucentMRTSupport = context.drawBuffers && extensionsSupported;
        this._translucentMultipassSupport = !this._translucentMRTSupport && extensionsSupported;

        this._opaqueFBO = undefined;
        this._opaqueTexture = undefined;
        this._depthStencilTexture = undefined;

        this._accumulationTexture = undefined;

        this._translucentFBO = undefined;
        this._alphaFBO = undefined;

        this._adjustTranslucentFBO = undefined;
        this._adjustAlphaFBO = undefined;

        this._opaqueClearCommand = new ClearCommand({
            color : new Color(0.0, 0.0, 0.0, 0.0),
            owner : this
        });
        this._translucentMRTClearCommand = new ClearCommand({
            color : new Color(0.0, 0.0, 0.0, 1.0),
            owner : this
        });
        this._translucentMultipassClearCommand = new ClearCommand({
            color : new Color(0.0, 0.0, 0.0, 0.0),
            owner : this
        });
        this._alphaClearCommand = new ClearCommand({
            color : new Color(1.0, 1.0, 1.0, 1.0),
            owner : this
        });

        this._translucentRenderStateCache = {};
        this._alphaRenderStateCache = {};

        this._compositeCommand = undefined;
        this._adjustTranslucentCommand = undefined;
        this._adjustAlphaCommand = undefined;

        this._viewport = new BoundingRectangle();
        this._rs = undefined;

        this._useScissorTest = false;
        this._scissorRectangle = undefined;

        this._useHDR = false;
    }

    function destroyTextures(oit) {
        oit._accumulationTexture = oit._accumulationTexture && !oit._accumulationTexture.isDestroyed() && oit._accumulationTexture.destroy();
        oit._revealageTexture = oit._revealageTexture && !oit._revealageTexture.isDestroyed() && oit._revealageTexture.destroy();
    }

    function destroyFramebuffers(oit) {
        oit._translucentFBO = oit._translucentFBO && !oit._translucentFBO.isDestroyed() && oit._translucentFBO.destroy();
        oit._alphaFBO = oit._alphaFBO && !oit._alphaFBO.isDestroyed() && oit._alphaFBO.destroy();
        oit._adjustTranslucentFBO = oit._adjustTranslucentFBO && !oit._adjustTranslucentFBO.isDestroyed() && oit._adjustTranslucentFBO.destroy();
        oit._adjustAlphaFBO = oit._adjustAlphaFBO && !oit._adjustAlphaFBO.isDestroyed() && oit._adjustAlphaFBO.destroy();
    }

    function destroyResources(oit) {
        destroyTextures(oit);
        destroyFramebuffers(oit);
    }

    function updateTextures(oit, context, width, height) {
        destroyTextures(oit);

        oit._accumulationTexture = new Texture({
            context : context,
            width : width,
            height : height,
            pixelFormat : PixelFormat.RGBA,
            pixelDatatype : PixelDatatype.FLOAT
        });

        // Use zeroed arraybuffer instead of null to initialize texture
        // to workaround Firefox. Only needed for the second color attachment.
        var source = new Float32Array(width * height * 4);
        oit._revealageTexture = new Texture({
            context : context,
            pixelFormat : PixelFormat.RGBA,
            pixelDatatype : PixelDatatype.FLOAT,
            source : {
                arrayBufferView : source,
                width : width,
                height : height
            },
            flipY : false
        });
    }

    function updateFramebuffers(oit, context) {
        destroyFramebuffers(oit);

        var completeFBO = WebGLConstants.FRAMEBUFFER_COMPLETE;
        var supported = true;

        // if MRT is supported, attempt to make an FBO with multiple color attachments
        if (oit._translucentMRTSupport) {
            oit._translucentFBO = new Framebuffer({
                context : context,
                colorTextures : [oit._accumulationTexture, oit._revealageTexture],
                depthStencilTexture : oit._depthStencilTexture,
                destroyAttachments : false
            });
            oit._adjustTranslucentFBO = new Framebuffer({
                context : context,
                colorTextures : [oit._accumulationTexture, oit._revealageTexture],
                destroyAttachments : false
            });

            if (oit._translucentFBO.status !== completeFBO || oit._adjustTranslucentFBO.status !== completeFBO) {
                destroyFramebuffers(oit);
                oit._translucentMRTSupport = false;
            }
        }

        // either MRT isn't supported or FBO creation failed, attempt multipass
        if (!oit._translucentMRTSupport) {
            oit._translucentFBO = new Framebuffer({
                context : context,
                colorTextures : [oit._accumulationTexture],
                depthStencilTexture : oit._depthStencilTexture,
                destroyAttachments : false
            });
            oit._alphaFBO = new Framebuffer({
                context : context,
                colorTextures : [oit._revealageTexture],
                depthStencilTexture : oit._depthStencilTexture,
                destroyAttachments : false
            });
            oit._adjustTranslucentFBO = new Framebuffer({
                context : context,
                colorTextures : [oit._accumulationTexture],
                destroyAttachments : false
            });
            oit._adjustAlphaFBO = new Framebuffer({
                context : context,
                colorTextures : [oit._revealageTexture],
                destroyAttachments : false
            });

            var translucentComplete = oit._translucentFBO.status === completeFBO;
            var alphaComplete = oit._alphaFBO.status === completeFBO;
            var adjustTranslucentComplete = oit._adjustTranslucentFBO.status === completeFBO;
            var adjustAlphaComplete = oit._adjustAlphaFBO.status === completeFBO;
            if (!translucentComplete || !alphaComplete || !adjustTranslucentComplete || !adjustAlphaComplete) {
                destroyResources(oit);
                oit._translucentMultipassSupport = false;
                supported = false;
            }
        }

        return supported;
    }

    OIT.prototype.update = function(context, passState, framebuffer, useHDR) {
        if (!this.isSupported()) {
            return;
        }

        this._opaqueFBO = framebuffer;
        this._opaqueTexture = framebuffer.getColorTexture(0);
        this._depthStencilTexture = framebuffer.depthStencilTexture;

        var width = this._opaqueTexture.width;
        var height = this._opaqueTexture.height;

        var accumulationTexture = this._accumulationTexture;
        var textureChanged = !defined(accumulationTexture) || accumulationTexture.width !== width || accumulationTexture.height !== height || useHDR !== this._useHDR;
        if (textureChanged) {
            updateTextures(this, context, width, height);
        }

        if (!defined(this._translucentFBO) || textureChanged) {
            if (!updateFramebuffers(this, context)) {
                // framebuffer creation failed
                return;
            }
        }

        this._useHDR = useHDR;

        var that = this;
        var fs;
        var uniformMap;

        if (!defined(this._compositeCommand)) {
            fs = new ShaderSource({
                sources : [CompositeOITFS]
            });
            if (this._translucentMRTSupport) {
                fs.defines.push('MRT');
            }

            uniformMap = {
                u_opaque : function() {
                    return that._opaqueTexture;
                },
                u_accumulation : function() {
                    return that._accumulationTexture;
                },
                u_revealage : function() {
                    return that._revealageTexture;
                }
            };
            this._compositeCommand = context.createViewportQuadCommand(fs, {
                uniformMap : uniformMap,
                owner : this
            });
        }

        if (!defined(this._adjustTranslucentCommand)) {
            if (this._translucentMRTSupport) {
                fs = new ShaderSource({
                    defines : ['MRT'],
                    sources : [AdjustTranslucentFS]
                });

                uniformMap = {
                    u_bgColor : function() {
                        return that._translucentMRTClearCommand.color;
                    },
                    u_depthTexture : function() {
                        return that._depthStencilTexture;
                    }
                };

                this._adjustTranslucentCommand = context.createViewportQuadCommand(fs, {
                    uniformMap : uniformMap,
                    owner : this
                });
            } else if (this._translucentMultipassSupport) {
                fs = new ShaderSource({
                    sources : [AdjustTranslucentFS]
                });

                uniformMap = {
                    u_bgColor : function() {
                        return that._translucentMultipassClearCommand.color;
                    },
                    u_depthTexture : function() {
                        return that._depthStencilTexture;
                    }
                };

                this._adjustTranslucentCommand = context.createViewportQuadCommand(fs, {
                    uniformMap : uniformMap,
                    owner : this
                });

                uniformMap = {
                    u_bgColor : function() {
                        return that._alphaClearCommand.color;
                    },
                    u_depthTexture : function() {
                        return that._depthStencilTexture;
                    }
                };

                this._adjustAlphaCommand = context.createViewportQuadCommand(fs, {
                    uniformMap : uniformMap,
                    owner : this
                });
            }
        }

        this._viewport.width = width;
        this._viewport.height = height;

        var useScissorTest = !BoundingRectangle.equals(this._viewport, passState.viewport);
        var updateScissor = useScissorTest !== this._useScissorTest;
        this._useScissorTest = useScissorTest;

        if (!BoundingRectangle.equals(this._scissorRectangle, passState.viewport)) {
            this._scissorRectangle = BoundingRectangle.clone(passState.viewport, this._scissorRectangle);
            updateScissor = true;
        }

        if (!defined(this._rs) || !BoundingRectangle.equals(this._viewport, this._rs.viewport) || updateScissor) {
            this._rs = RenderState.fromCache({
                viewport : this._viewport,
                scissorTest : {
                    enabled : this._useScissorTest,
                    rectangle : this._scissorRectangle
                }
            });
        }

        if (defined(this._compositeCommand)) {
            this._compositeCommand.renderState = this._rs;
        }

        if (this._adjustTranslucentCommand) {
            this._adjustTranslucentCommand.renderState = this._rs;
        }

        if (defined(this._adjustAlphaCommand)) {
            this._adjustAlphaCommand.renderState = this._rs;
        }
    };

    var translucentMRTBlend = {
        enabled : true,
        color : new Color(0.0, 0.0, 0.0, 0.0),
        equationRgb : BlendEquation.ADD,
        equationAlpha : BlendEquation.ADD,
        functionSourceRgb : BlendFunction.ONE,
        functionDestinationRgb : BlendFunction.ONE,
        functionSourceAlpha : BlendFunction.ZERO,
        functionDestinationAlpha : BlendFunction.ONE_MINUS_SOURCE_ALPHA
    };

    var translucentColorBlend = {
        enabled : true,
        color : new Color(0.0, 0.0, 0.0, 0.0),
        equationRgb : BlendEquation.ADD,
        equationAlpha : BlendEquation.ADD,
        functionSourceRgb : BlendFunction.ONE,
        functionDestinationRgb : BlendFunction.ONE,
        functionSourceAlpha : BlendFunction.ONE,
        functionDestinationAlpha : BlendFunction.ONE
    };

    var translucentAlphaBlend = {
        enabled : true,
        color : new Color(0.0, 0.0, 0.0, 0.0),
        equationRgb : BlendEquation.ADD,
        equationAlpha : BlendEquation.ADD,
        functionSourceRgb : BlendFunction.ZERO,
        functionDestinationRgb : BlendFunction.ONE_MINUS_SOURCE_ALPHA,
        functionSourceAlpha : BlendFunction.ZERO,
        functionDestinationAlpha : BlendFunction.ONE_MINUS_SOURCE_ALPHA
    };

    function getTranslucentRenderState(context, translucentBlending, cache, renderState) {
        var translucentState = cache[renderState.id];
        if (!defined(translucentState)) {
            var rs = RenderState.getState(renderState);
            rs.depthMask = false;
            rs.blending = translucentBlending;

            translucentState = RenderState.fromCache(rs);
            cache[renderState.id] = translucentState;
        }

        return translucentState;
    }

    function getTranslucentMRTRenderState(oit, context, renderState) {
        return getTranslucentRenderState(context, translucentMRTBlend, oit._translucentRenderStateCache, renderState);
    }

    function getTranslucentColorRenderState(oit, context, renderState) {
        return getTranslucentRenderState(context, translucentColorBlend, oit._translucentRenderStateCache, renderState);
    }

    function getTranslucentAlphaRenderState(oit, context, renderState) {
        return getTranslucentRenderState(context, translucentAlphaBlend, oit._alphaRenderStateCache, renderState);
    }

    var mrtShaderSource =
        '    vec3 Ci = czm_gl_FragColor.rgb * czm_gl_FragColor.a;\n' +
        '    float ai = czm_gl_FragColor.a;\n' +
        '    float wzi = czm_alphaWeight(ai);\n' +
        '    gl_FragData[0] = vec4(Ci * wzi, ai);\n' +
        '    gl_FragData[1] = vec4(ai * wzi);\n';

    var colorShaderSource =
        '    vec3 Ci = czm_gl_FragColor.rgb * czm_gl_FragColor.a;\n' +
        '    float ai = czm_gl_FragColor.a;\n' +
        '    float wzi = czm_alphaWeight(ai);\n' +
        '    gl_FragColor = vec4(Ci, ai) * wzi;\n';

    var alphaShaderSource =
        '    float ai = czm_gl_FragColor.a;\n' +
        '    gl_FragColor = vec4(ai);\n';

    function getTranslucentShaderProgram(context, shaderProgram, keyword, source) {
        var shader = context.shaderCache.getDerivedShaderProgram(shaderProgram, keyword);
        if (!defined(shader)) {
            var attributeLocations = shaderProgram._attributeLocations;

            var fs = shaderProgram.fragmentShaderSource.clone();

            fs.sources = fs.sources.map(function(source) {
                source = ShaderSource.replaceMain(source, 'czm_translucent_main');
                source = source.replace(/gl_FragColor/g, 'czm_gl_FragColor');
                source = source.replace(/\bdiscard\b/g, 'czm_discard = true');
                source = source.replace(/czm_phong/g, 'czm_translucentPhong');
                return source;
            });

            // Discarding the fragment in main is a workaround for ANGLE D3D9
            // shader compilation errors.

            fs.sources.splice(0, 0,
                (source.indexOf('gl_FragData') !== -1 ? '#extension GL_EXT_draw_buffers : enable \n' : '') +
                'vec4 czm_gl_FragColor;\n' +
                'bool czm_discard = false;\n');

            fs.sources.push(
                'void main()\n' +
                '{\n' +
                '    czm_translucent_main();\n' +
                '    if (czm_discard)\n' +
                '    {\n' +
                '        discard;\n' +
                '    }\n' +
                source +
                '}\n');

            shader = context.shaderCache.createDerivedShaderProgram(shaderProgram, keyword, {
                vertexShaderSource : shaderProgram.vertexShaderSource,
                fragmentShaderSource : fs,
                attributeLocations : attributeLocations
            });
        }

        return shader;
    }

    function getTranslucentMRTShaderProgram(context, shaderProgram) {
        return getTranslucentShaderProgram(context, shaderProgram, 'translucentMRT', mrtShaderSource);
    }

    function getTranslucentColorShaderProgram(context, shaderProgram) {
        return getTranslucentShaderProgram(context, shaderProgram, 'translucentMultipass', colorShaderSource);
    }

    function getTranslucentAlphaShaderProgram(context, shaderProgram) {
        return getTranslucentShaderProgram(context, shaderProgram, 'alphaMultipass', alphaShaderSource);
    }

    OIT.prototype.createDerivedCommands = function(command, context, result) {
        if (!defined(result)) {
            result = {};
        }

        if (this._translucentMRTSupport) {
            var translucentShader;
            var translucentRenderState;
            if (defined(result.translucentCommand)) {
                translucentShader = result.translucentCommand.shaderProgram;
                translucentRenderState = result.translucentCommand.renderState;
            }

            result.translucentCommand = DrawCommand.shallowClone(command, result.translucentCommand);

            if (!defined(translucentShader) || result.shaderProgramId !== command.shaderProgram.id) {
                result.translucentCommand.shaderProgram = getTranslucentMRTShaderProgram(context, command.shaderProgram);
                result.translucentCommand.renderState = getTranslucentMRTRenderState(this, context, command.renderState);
                result.shaderProgramId = command.shaderProgram.id;
            } else {
                result.translucentCommand.shaderProgram = translucentShader;
                result.translucentCommand.renderState = translucentRenderState;
            }
        } else {
            var colorShader;
            var colorRenderState;
            var alphaShader;
            var alphaRenderState;
            if (defined(result.translucentCommand)) {
                colorShader = result.translucentCommand.shaderProgram;
                colorRenderState = result.translucentCommand.renderState;
                alphaShader = result.alphaCommand.shaderProgram;
                alphaRenderState = result.alphaCommand.renderState;
            }

            result.translucentCommand = DrawCommand.shallowClone(command, result.translucentCommand);
            result.alphaCommand = DrawCommand.shallowClone(command, result.alphaCommand);

            if (!defined(colorShader) || result.shaderProgramId !== command.shaderProgram.id) {
                result.translucentCommand.shaderProgram = getTranslucentColorShaderProgram(context, command.shaderProgram);
                result.translucentCommand.renderState = getTranslucentColorRenderState(this, context, command.renderState);
                result.alphaCommand.shaderProgram = getTranslucentAlphaShaderProgram(context, command.shaderProgram);
                result.alphaCommand.renderState = getTranslucentAlphaRenderState(this, context, command.renderState);
                result.shaderProgramId = command.shaderProgram.id;
            } else {
                result.translucentCommand.shaderProgram = colorShader;
                result.translucentCommand.renderState = colorRenderState;
                result.alphaCommand.shaderProgram = alphaShader;
                result.alphaCommand.renderState = alphaRenderState;
            }
        }

        return result;
    };

    function executeTranslucentCommandsSortedMultipass(oit, scene, executeFunction, passState, commands, invertClassification) {
        var command;
        var derivedCommand;
        var j;

        var context = scene.context;
        var useLogDepth = scene.frameState.useLogDepth;
        var useHdr = scene._hdr;
        var framebuffer = passState.framebuffer;
        var length = commands.length;

        var lightShadowsEnabled = scene.frameState.shadowState.lightShadowsEnabled;

        passState.framebuffer = oit._adjustTranslucentFBO;
        oit._adjustTranslucentCommand.execute(context, passState);
        passState.framebuffer = oit._adjustAlphaFBO;
        oit._adjustAlphaCommand.execute(context, passState);

        var debugFramebuffer = oit._opaqueFBO;
        passState.framebuffer = oit._translucentFBO;

        for (j = 0; j < length; ++j) {
            command = commands[j];
            command = useLogDepth ? command.derivedCommands.logDepth.command : command;
            command = useHdr ? command.derivedCommands.hdr.command : command;
            derivedCommand = (lightShadowsEnabled && command.receiveShadows) ? command.derivedCommands.oit.shadows.translucentCommand : command.derivedCommands.oit.translucentCommand;
            executeFunction(derivedCommand, scene, context, passState, debugFramebuffer);
        }

        if (defined(invertClassification)) {
            command = invertClassification.unclassifiedCommand;
            derivedCommand = (lightShadowsEnabled && command.receiveShadows) ? command.derivedCommands.oit.shadows.translucentCommand : command.derivedCommands.oit.translucentCommand;
            executeFunction(derivedCommand, scene, context, passState, debugFramebuffer);
        }

        passState.framebuffer = oit._alphaFBO;

        for (j = 0; j < length; ++j) {
            command = commands[j];
            command = useLogDepth ? command.derivedCommands.logDepth.command : command;
            command = useHdr ? command.derivedCommands.hdr.command : command;
            derivedCommand = (lightShadowsEnabled && command.receiveShadows) ? command.derivedCommands.oit.shadows.alphaCommand : command.derivedCommands.oit.alphaCommand;
            executeFunction(derivedCommand, scene, context, passState, debugFramebuffer);
        }

        if (defined(invertClassification)) {
            command = invertClassification.unclassifiedCommand;
            derivedCommand = (lightShadowsEnabled && command.receiveShadows) ? command.derivedCommands.oit.shadows.alphaCommand : command.derivedCommands.oit.alphaCommand;
            executeFunction(derivedCommand, scene, context, passState, debugFramebuffer);
        }

        passState.framebuffer = framebuffer;
    }

    function executeTranslucentCommandsSortedMRT(oit, scene, executeFunction, passState, commands, invertClassification) {
        var context = scene.context;
        var useLogDepth = scene.frameState.useLogDepth;
        var useHdr = scene._hdr;
        var framebuffer = passState.framebuffer;
        var length = commands.length;

        var lightShadowsEnabled = scene.frameState.shadowState.lightShadowsEnabled;

        passState.framebuffer = oit._adjustTranslucentFBO;
        oit._adjustTranslucentCommand.execute(context, passState);

        var debugFramebuffer = oit._opaqueFBO;
        passState.framebuffer = oit._translucentFBO;

        var command;
        var derivedCommand;

        for (var j = 0; j < length; ++j) {
            command = commands[j];
            command = useLogDepth ? command.derivedCommands.logDepth.command : command;
            command = useHdr ? command.derivedCommands.hdr.command : command;
            derivedCommand = (lightShadowsEnabled && command.receiveShadows) ? command.derivedCommands.oit.shadows.translucentCommand : command.derivedCommands.oit.translucentCommand;
            executeFunction(derivedCommand, scene, context, passState, debugFramebuffer);
        }

        if (defined(invertClassification)) {
            command = invertClassification.unclassifiedCommand;
            derivedCommand = (lightShadowsEnabled && command.receiveShadows) ? command.derivedCommands.oit.shadows.translucentCommand : command.derivedCommands.oit.translucentCommand;
            executeFunction(derivedCommand, scene, context, passState, debugFramebuffer);
        }

        passState.framebuffer = framebuffer;
    }

    OIT.prototype.executeCommands = function(scene, executeFunction, passState, commands, invertClassification) {
        if (this._translucentMRTSupport) {
            executeTranslucentCommandsSortedMRT(this, scene, executeFunction, passState, commands, invertClassification);
            return;
        }

        executeTranslucentCommandsSortedMultipass(this, scene, executeFunction, passState, commands, invertClassification);
    };

    OIT.prototype.execute = function(context, passState) {
        this._compositeCommand.execute(context, passState);
    };

    OIT.prototype.clear = function(context, passState, clearColor) {
        var framebuffer = passState.framebuffer;

        passState.framebuffer = this._opaqueFBO;
        Color.clone(clearColor, this._opaqueClearCommand.color);
        this._opaqueClearCommand.execute(context, passState);

        passState.framebuffer = this._translucentFBO;
        var translucentClearCommand = this._translucentMRTSupport ? this._translucentMRTClearCommand : this._translucentMultipassClearCommand;
        translucentClearCommand.execute(context, passState);

        if (this._translucentMultipassSupport) {
            passState.framebuffer = this._alphaFBO;
            this._alphaClearCommand.execute(context, passState);
        }

        passState.framebuffer = framebuffer;
    };

    OIT.prototype.isSupported = function() {
        return this._translucentMRTSupport || this._translucentMultipassSupport;
    };

    OIT.prototype.isDestroyed = function() {
        return false;
    };

    OIT.prototype.destroy = function() {
        destroyResources(this);

        if (defined(this._compositeCommand)) {
            this._compositeCommand.shaderProgram = this._compositeCommand.shaderProgram && this._compositeCommand.shaderProgram.destroy();
        }

        if (defined(this._adjustTranslucentCommand)) {
            this._adjustTranslucentCommand.shaderProgram = this._adjustTranslucentCommand.shaderProgram && this._adjustTranslucentCommand.shaderProgram.destroy();
        }

        if (defined(this._adjustAlphaCommand)) {
            this._adjustAlphaCommand.shaderProgram = this._adjustAlphaCommand.shaderProgram && this._adjustAlphaCommand.shaderProgram.destroy();
        }

        return destroyObject(this);
    };
export default OIT;
